<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta http-equiv=X-UA-Compatible content="IE=edge">
  <meta name=viewport content="width=device-width,initial-scale=1">
  <title>配置工具</title>
  <link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/vant@2.10/lib/index.css" />
  <script src="https://fastly.jsdelivr.net/npm/vue@2.6/dist/vue.min.js"></script>
  <!-- <script src="https://fastly.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
  <script src="https://unpkg.com/vue-router@3.4.9/dist/vue-router.js"></script>
  <script src="https://unpkg.com/vuex@3.6.2/dist/vuex.js"></script>
  <script src="https://fastly.jsdelivr.net/npm/vant@2.10/lib/vant.min.js"></script>
  <script src="https://fastly.jsdelivr.net/npm/vconsole@3.11.2/dist/vconsole.min.js"></script>
</head>
<style>
  .wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
  }

  .floaty-btn {
    position: fixed;
    right: 21px;
  }

  .floaty-btn img {
    width: 30px;
    height: 30px;
    content: url();
  }

  .to-top {
    bottom: 65px;
    /* transform: rotate(-90deg); */
  }

  .to-bottom {
    bottom: 35px;
    transform: rotate(180deg);
  }

  .wd-break {
    word-break: break-all;
    white-space: pre-line;
  }

  /* .low-border-radius { */
  .van-dialog {
    border-radius: 4px;
  }

  .fixed-div {
    position: fixed;
    width: 100%;
    z-index: 999;
  }

  .search {
    margin-top: 2px;
  }

  .log-list {
    position: absolute;
    min-height: 500px;
    height: calc(100% - 60px);
    width: 100%;
    margin-top: 60px;
  }

  .file-container {
    display: flex;
    align-items: center;
  }

  .file-container img {
    width: 20px;
    height: 20px;
    margin-right: 5px;
  }

  .up-dir {
    content: url();
  }

  .folder {
    content: url();
  }

  .file-cell .log {
    content: url();
  }

  .unknown {
    content: url()
  }


  .file-cell::before {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    background-color: rgb(75, 75, 75);
    border: inherit;
    border-color: rgb(75, 75, 75);
    border-radius: inherit;
    transform: translate(-50%, -50%);
    opacity: 0;
    content: ' ';
  }

  .file-cell:active::before {
    opacity: 0.1;
  }

  .file-size {
    flex: 1;
    text-align: right;
    margin-right: 0.05rem;
    color: gray;
    font-size: 0.15rem;
  }

  .wd-break.error {
    color: red;
  }

  .wd-break.warn {
    color: rgb(0, 0, 255);
  }

  .wd-break.info {
    color: rgb(0, 255, 55);
  }

  .wd-break.log {
    color: rgb(0, 0, 0);
  }

  .wd-break.debug {
    color: #969799;
  }

  html {
    height: 100%;
  }

  body {
    /* important */
    margin: 0 0;
    height: 100%;
  }
</style>

<body>
  <div id="app" style="min-height:700px;position: relative;height:100%">
    <div class="fixed-div">
      <van-progress :percentage="readPrecent" stroke-width="2" :show-pivot="false" />
    </div>
    <div class="fixed-div search">
      <van-search v-model="searchValue" placeholder="请输入搜索关键词" @search="search" show-action>
        <template #action>
          <div @click="showDialog=true">设置</div>
        </template>
      </van-search>
    </div>
    <div class="log-list">
      <van-pull-refresh v-model="refreshing" @refresh="onRefresh" style="overflow: scroll;height: 100%;">
        <div ref="top-block" id="top-block"></div>
        <van-cell v-for="(item,idx) in displayLogList" :key="item.origin" :title="item.logContent"
          class="van-clearfix log-item" :ref="backHere && item.origin==backHere?'scroll-back':''"
          :title-class="'wd-break ' + coloredTxtIfNeeded(item)" @click="showLogDetail(item,idx)"
          :label="idx + ': ' + item.dateInfo">
        </van-cell>
        <div ref="bottom-block"></div>
      </van-pull-refresh>
    </div>
    <div class="floaty-btn to-top" @click="toTop">
      <img />
    </div>
    <div class="floaty-btn to-bottom" @click="toBottom">
      <img />
    </div>
    <van-overlay :show="loading" z-index="1000">
      <div class="wrapper">
        <van-loading size="4rem" vertical>加载中...</van-loading>
      </div>
    </van-overlay>
    <van-popup v-model="logDetailPopup" position="bottom" :style="{ height: '60%' }" get-container="#app">
      <div class="wd-break" style="display: flex;padding: 2rem; justify-content:center; align-items: center;">
        {{logDetails}}
      </div>
    </van-popup>
    <van-dialog v-model="showDialog" title="设置" :show-confirm-button="true" close-on-click-overlay get-container="#app">
      <div style="width: 100%;min-height: 300px;overflow: scroll;">
        <van-cell-group>
          <van-swipe-cell stop-propagation>
            <van-field label="日志文件路径" type="textarea" v-model="readFilePath" autosize readonly class="wd-break">
            </van-field>
            <template #right>
              <van-button square type="primary" text="修改" style="height: 100%;" @click="showFileSelector" />
            </template>
          </van-swipe-cell>
          <van-field label="日志文件过滤正则" label-width="8rem" v-model="logFileMatcher" input-align="right"></van-field>
          <van-field label="当前日志文件大小" label-width="8rem" readonly
            :value="this.total?((this.total/1024).toFixed(2)+'KB') : '未加载'" input-align="right"></van-field>
          <van-field label="单次读取行数" v-model="singleReadLine" input-align="right"></van-field>
          <van-cell center title="彩色显示">
            <van-switch v-model="colored" size="1.24rem" />
          </van-cell>
          <van-cell center title="切换VConsole">
            <van-switch v-model="showVConsole" size="1.24rem" />
          </van-cell>
        </van-cell-group>
      </div>
    </van-dialog>
    <van-dialog v-model="showFileSelectDialog" title="选择日志文件" :show-confirm-button="false" close-on-click-overlay
      get-container="#app">
      <div style="width: 100%;height: 400px;overflow: scroll;">
        <van-cell-group>
          <van-cell v-for="file in files" class="file-cell" @click="selectFile(file)">
            <template #title>
              <div class="van-cell__title file-container">
                <img v-if="file.type==='parent'" class="up-dir">
                <img v-else-if="file.type==='dir'" class="folder">
                <img v-else-if="file.type==='log' || file.type==='txt'" class="log">
                <img v-else class="unknown">
                <span>{{file.name}}</span>
                <span v-if="file.type !== 'dir' && file.type !== 'parent'" class="file-size">{{file.fileSize | fileSizeStr}}</span>
              </div>
            </template>
          </van-cell>
        </van-cell-group>
      </div>
    </van-dialog>
  </div>
</body>
<script type="text/javascript">

  /**
   * 禁用长按菜单，复制等 
   **/
  // window.addEventListener('contextmenu', function (e) {
  //   e.preventDefault()
  // })
  let currentHeight = 0
  window.addEventListener('resize', function (e) {
    console.log('触发resize')
    if (window.innerHeight > currentHeight) {
      currentHeight = window.innerHeight
      document.getElementById('app').style.height = window.innerHeight + 'px'
    }
  }, false)
  // window.onload = () => {
  // 直接浏览器调试时，mock js bridge方法
  if (typeof $app === 'undefined') {
    window.$app = {
      invoke: (bridgeName, data, callback) => { },
      receiveMessage: params => { },
      registerFunction: (functionName, func) => { }
    }
    window.$nativeApi = {
      request: (bridgeName, data) => {
        if (bridgeName === 'loadLogs') {
          return Promise.resolve({
            // PC端测试用数据
            readResult: {
              total: 10000,
              result: [
                '2021-12-22 17:25:50,047 - [DEBUG]	[25865]  - 17:25:50.047/V: 当前包名：com.eg.android.AlipayGphone 对比包名: com.eg.android.AlipayGphone',
                '2021-12-22 17:25:50,101 - [DEBUG]	[25865]  - 17:25:50.101/V: windowRoots size: 1',
                '2021-12-22 17:25:50,104 - [DEBUG]	[25865]  - 17:25:50.103/V: 获取包名总耗时：55ms',
                '2021-12-22 17:25:50,117 - [DEBUG]	[25865]  - 17:25:50.117/V: try to find one: 返回|关闭 timeout: 500ms',
                '2021-12-22 17:25:50,630 - [DEBUG]	[25865]  - 17:25:50.627/V: cannot find any matches 返回|关闭 timeout:500',
                '2021-12-22 17:25:50,634 - [WARN]	[25865]  - 17:25:50.632/W: timeout for finding 返回|关闭',
                '2021-12-22 17:25:51,154 - [DEBUG]	[25865]  - 17:25:51.153/V: windowRoots size: 1',
                '2021-12-22 17:25:51,155 - [DEBUG]	[25865]  - 17:25:51.155/V: 获取包名总耗时：17ms',
                '2021-12-22 17:25:51,157 - [DEBUG]	[25865]  - 17:25:51.156/V: 当前包名：com.taobao.idlefishs.modify.opencv4 对比包名: com.eg.android.AlipayGphone',
                '2021-12-22 17:25:51,181 - [DEBUG]	[25865]  - 17:25:51.180/V: windowRoots size: 1',
                '2021-12-22 17:25:51,200 - [DEBUG]	[25865]  - 17:25:51.190/V: 获取包名总耗时：25ms',
                '2021-12-22 17:25:51,228 - [DEBUG]	[25865]  - 17:25:51.228/V: windowRoots size: 1',
                '2021-12-22 17:25:51,232 - [DEBUG]	[25865]  - 17:25:51.232/V: 获取包名总耗时：30ms',
                '2021-12-22 17:25:51,240 - [DEBUG]	[25865]  - 17:25:51.233/V: 通话状态监听功能当前启用状态：true',
                '2021-12-22 17:25:51,242 - [DEBUG]	[25865]  - 17:25:51.241/V: 移除phoneStateBroadcastReceiver',
                '2021-12-22 17:25:51,245 - [DEBUG]	[25865]  - 17:25:51.244/V: 释放图片，总数：125',
                '2021-12-22 17:25:51,362 - [DEBUG]	[25865]  - 17:25:51.361/D: 是否需要重新锁定屏幕：false',
                '2021-12-22 17:25:51,364 - [DEBUG]	[25865]  - 17:25:51.363/V: 获取运行中任务信息：{"source":"/storage/emulated/0/脚本/energy_store/main.js","engineId":4,"timeout":1640165994575} 超时剩余时间：843秒',
                '2021-12-22 17:25:51,366 - [DEBUG]	[25865]  - 17:25:51.365/V: 准备移除运行中任务',
                '2021-12-22 17:25:51,377 - [DEBUG]	[25865]  - 17:25:51.376/V: 队列小于等于1 不需要去重:[]',
                '2021-12-22 17:25:51,381 - [DEBUG]	[25865]  - 17:25:51.380/V: 无任务等待中',
                '2021-12-22 17:25:51,384 - [DEBUG]	[25865]  - 17:25:51.383/V: 设置脚本状态为执行完毕',
                '2021-12-22 17:25:51,387 - [DEBUG]	[25865]  - 17:25:51.387/V: 等待scanner线程池关闭, 结果: true',
                '2021-12-22 17:25:51,389 - [DEBUG]	[25865]  - 17:25:51.389/V: 移除生命周期回调，id:6 index:5 desc: shutdown scanner thread pool',
                '2021-12-22 17:25:51,392 - [DEBUG]	[25865]  - 17:25:51.392/V: 当前日志列表长度： 644',
                '2021-12-22 17:25:51,393 - [DEBUG]	[25865]  - 17:25:51.393/D: ========本轮结束========',
                '2021-12-22 17:25:51,396 - [DEBUG]	[25865]  - 17:25:51.395/V: 当前周期内增加了：748 g 未达到冷却值: 1000',
                '2021-12-22 17:25:51,397 - [DEBUG]	[25865]  - 17:25:51.397/V: 获取到的倒计时时间：20',
                '2021-12-22 17:25:51,399 - [DEBUG]	[25865]  - 17:25:51.398/V: 倒计时：20 ',
              ],
              readIndex: 6000
            }
          })
        }
        return Promise.reject({})
      }
    }
  }
  // }
</script>
<script type="text/javascript">

  Vue.use(vant)
  let app = new Vue({
    el: '#app',
    data() {
      return {
        readFilePath: null,
        currentSelectPath: null,
        logFileMatcher: '\.(log|txt)(\.\d+)?$',
        showVConsole: false,
        colored: true,
        singleReadLine: 500,
        backHere: null,
        refreshing: false,
        loading: false,
        searchValue: '',
        showDialog: false,
        showFileSelectDialog: false,
        logList: [],
        displayLogList: [],
        isFirst: true,
        readIndex: null,
        total: null,
        logDetails: '',
        logDetailPopup: false,
        VALID_DATE_LOG: /^\d{4}(-\d{2}){2}/,
        files: [
          { name: '..', type: 'parent' },
          { name: 'logs/', type: 'dir' },
          { name: '脚本/', type: 'dir' },
          { name: 'logs/', type: 'dir' },
          { name: '脚本/', type: 'dir' },
          { name: 'logs/', type: 'dir' },
          { name: '脚本/', type: 'dir' },
          { name: 'logs/', type: 'dir' },
          { name: '脚本/', type: 'dir' },
          { name: 'logs/', type: 'dir' },
          { name: '脚本/', type: 'dir' },
          { name: 'login.log', type: 'log', fileSize: 123339 },
          { name: 'log.txt', type: 'txt', fileSize: 111 },
          { name: 'data', type: 'unknown', fileSize: 123339123339 },
        ]
      }
    },
    filters: {
      fileSizeStr: fileSize => {
        if (!fileSize) {
          return '0 B'
        } else {
          return fileSize < 1024 ? fileSize + ' B' :
            fileSize < 1024 * 1024 ? (fileSize / 1024).toFixed(2) + ' KB' :
              fileSize < 1024 * 1024 * 1024 ? (fileSize / 1024 / 1024).toFixed(2) + ' MB' : (fileSize / 1024 / 1024 / 1024).toFixed(2) + ' GB'
        }
      }
    },
    computed: {
      readPrecent: function () {
        if (this.total) {
          return (this.total - this.readIndex) / this.total * 100
        }
        return 0
      }
    },
    methods: {
      coloredTxtIfNeeded: function (item) {
        if (this.colored) {
          switch (item.logLevel) {
            case 'DEBUG':
              return 'debug'
            case 'WARN':
              return 'warn'
            case 'LOG':
              return 'log'
            case 'INFO':
              return 'info'
            case 'ERROR':
            case 'ASSERT':
              return 'error'
          }
        }
        return ''
      },
      toTop: function () {
        this.$refs['top-block'].scrollIntoView({ behavior: "smooth" })
      },
      toBottom: function () {
        this.$refs['bottom-block'].scrollIntoView({ behavior: "smooth" })
      },
      doLoad: function () {
        this.finished = true
      },
      appendErrorLogIfPossible: function (content, idx) {
        if (this.VALID_DATE_LOG.test(content.origin)) {
          console.log('当前日志为正常的', content.origin, idx)
          this.logDetails = content.origin
          this.appendPrependIfPossible(idx)
        } else {
          console.log('当前日志可能为非正常的', content.origin, idx)
          this.logDetails = ''
          this.appendPreviousIfPossible(idx)
          this.logDetails += content.origin
          this.appendPrependIfPossible(idx)
          console.log('最终拼接日志', this.logDetails)
        }
      },
      /**
       * 向前寻找错误日志信息，直到找到带日期的正常日志并全部追加到日志详情
       */
      appendPreviousIfPossible: function (idx) {
        if (idx <= 0) {
          return
        }
        let firstValidIdx = idx - 1
        let curIdx
        for (curIdx = firstValidIdx; curIdx >= 0; curIdx--) {
          if (this.VALID_DATE_LOG.test(this.logList[curIdx])) {
            firstValidIdx = curIdx
            break
          }
        }
        if (curIdx < 0 && !this.VALID_DATE_LOG.test(this.logList[0])) {
          firstValidIdx = 0
        }
        for (curIdx = firstValidIdx; curIdx < idx; curIdx++) {
          console.log('前置追加日志：' + this.logList[curIdx])
          this.logDetails += this.logList[curIdx] + '\n'
        }
      },
      /**
       * 向后寻找错误日志信息，直到找到带日期的正常日志并将错误日志全部追加到日志详情
       */
      appendPrependIfPossible: function (idx) {
        let totalLength = this.logList.length
        if (idx >= totalLength - 1) {
          return
        }
        for (let curIdx = idx + 1; curIdx < totalLength; curIdx++) {
          if (this.VALID_DATE_LOG.test(this.logList[curIdx])) {
            break
          }
          console.log('后置追加日志：' + this.logList[curIdx])
          this.logDetails += '\n' + this.logList[curIdx]
        }
      },
      showLogDetail: function (content, idx) {
        this.appendErrorLogIfPossible(content, idx)
        this.logDetailPopup = true
      },
      search: function () {
        if (this.searchValue && this.logList.length > 0) {
          let searchRegex = new RegExp(this.searchValue, 'i')
          this.loading = true
          this.displayLogList = this.logList.filter(log => searchRegex.test(log))
          this.loading = false
        } else {
          this.displayLogList = this.logList
        }
        // 读取log4j日志时匹配 idx:5
        let dateRegex = /(((\d{2}:){2}(\d{2}\.\d{0,3}))\/(\w)): /
        // 读取log-verboses日志时匹配 idx:5
        let verboseLog = /(((\d{2}:){2}(\d{2}\.\d{0,3})) (\w+)) \[/
        // processId
        let pidRegex = /\[\w+\]\t\[(\d+)\]/
        this.displayLogList = this.displayLogList.map((v, idx) => ({
          origin: v,
          // 简化日志
          simplify: v.replace(/ - .* - ((\d{2}:){2}(\d{2}\.\d{0,3}))?/, ' '),
          // 日志内容信息 去除日期、类名等
          logContent: (() => {
            let findIdx = v.lastIndexOf(' - ')
            if (findIdx > -1) {
              let contentStartIdx = findIdx + 3
              return v.substring(contentStartIdx, v.length).replace(dateRegex, '')
            }
            return v
          })(),
          // 日志日期信息
          dateInfo: (() => {
            // 读取log4j日志时匹配
            let execResult = dateRegex.exec(v)
            if (!execResult || execResult.length === 0) {
              // 读取log-verboses日志是匹配
              execResult = verboseLog.exec(v)
            }
            let pid = ((r) => r && r.length > 0 ? ' ' + r[1] : '')(pidRegex.exec(v))
            if (execResult && execResult.length > 0) {
              return v.substring(0, 11) + execResult[1] + pid
            } else {
              return v.substring(0, 23) + pid
            }
          })(),
          logLevel: (() => {
            let execResult = dateRegex.exec(v)
            if (execResult && execResult.length > 0) {
              let identify = execResult[5]
              switch (identify) {
                case 'V':
                  return 'DEBUG'
                case 'D':
                  return 'LOG'
                case 'I':
                  return 'INFO'
                case 'W':
                  return 'WARN'
                case 'E':
                  return 'ERROR'
                case 'A':
                  return 'ASSERT'
                default:
                  return 'UNKNOWN'
              }
            }
            // 读取log-verboses日志是匹配
            execResult = verboseLog.exec(v)
            if (execResult && execResult.length > 0) {
              return execResult[5]
            }
            return 'UNKNOWN'
          })()
        }))
      },
      onRefresh: function () {
        if (this.loading) {
          console.log('refresh加载中', this.loading, this.refreshing)
          return
        }
        //
        this.refreshing = true
        this.loadLogs()
      },
      loadLogs: function () {
        if (this.isFirst) {
          this.readIndex = null
          this.logList = []
        }
        if (this.loading && !this.isFirst) {
          console.log('load加载中', this.loading, this.refreshing)
          return
        }
        if (this.readIndex === 0) {
          $app.invoke('toastLog', { message: '已加载完毕，文件总大小：' + (this.total / 1024) + 'KB' })
          this.refreshing = false
          return
        }
        this.loading = true
        console.log('准备加载日志内容')
        $nativeApi.request('loadLogs', { start: this.readIndex, num: this.singleReadLine, logPath: this.readFilePath }).then(({ readResult }) => {
          if (readResult) {
            if (this.logList.length > 0) {
              this.backHere = this.logList[0]
            }
            this.logList = readResult.result.concat(this.logList)
            this.readIndex = readResult.readIndex
          } else {
            $app.invoke('toastLog', { message: '已加载完毕' })
          }
          console.log('加载日志内容完毕', !!readResult && !!readResult.result ? readResult.result.length : '读取失败', 'readIndex:', readResult.readIndex)
          this.search()
          this.loading = false
          this.refreshing = false
          if (this.isFirst) {
            // 首次加载的文件，滑动到最底部 保存最大长度等信息
            this.total = readResult.total
            this.isFirst = false
            this.readFilePath = readResult.filePath || this.readFilePath || '/storage/emulated/0/脚本/energy_store/logs/log-verboses.log'
            this.currentSelectPath = this.readFilePath
            this.listFiles()
            console.log('注册上划', '文件总长度', this.total)
            console.log('displayLogList length :', (this.displayLogList || []).length)
            $app.invoke('toastLog', { message: '已加载完毕' })
            this.$nextTick(function () {
              console.log('触发上划')
              console.log(this.$refs['bottom-block'])
              this.$refs['bottom-block'].scrollIntoView()
            })
          } else {
            // 非首次加载的 将上一次读取位置保持在屏幕内
            this.$nextTick(function () {
              console.log('触发回归')
              console.log(this.$refs['scroll-back'])
              if (this.$refs['scroll-back']) {
                this.$refs['scroll-back'][0].scrollIntoView()
              }
            })
          }
        }).catch(e => {
          // 测试用
          console.log('发生异常', e)
          this.displayLogList = this.logList
          this.loading = false
          this.refreshing = false
        })
      },
      showFileSelector: function () {
        this.showFileSelectDialog = true
        this.showDialog = false
        this.listFiles(this.readFilePath)
      },
      listFiles: function (path) {
        $nativeApi.request('listLogFiles', { filePath: path || this.currentSelectPath, logFileMatcher: this.logFileMatcher }).then(({ fileResult }) => {
          if (fileResult.error) {
            $app.invoke('toastLog', { message: fileResult.error })
            return
          }
          this.files = [{ name: '..', type: 'parent' }].concat(fileResult.resultList || [])
          this.currentSelectPath = fileResult.path
        })
      },
      selectFile: function (file) {
        let selectedFilePath = this.currentSelectPath + '/' + file.name
        if (file.isDir) {
          this.listFiles(selectedFilePath)
        } else {
          if (file.name === '..') {
            this.listFiles(this.currentSelectPath.substring(0, this.currentSelectPath.lastIndexOf('/')))
            return
          }
          // $app.invoke('toastLog', { message: '选择了文件' + selectedFilePath })
          this.$dialog.confirm({
            message: '是否加载该文件？'
          }).then(() => {
            this.readFilePath = selectedFilePath
            this.showFileSelectDialog = false
            this.isFirst = true
            this.loadLogs()
          }).catch(() => { })
        }
      }
    },
    watch: {
      showVConsole: function (newVal) {
        if (newVal) {
          window.vConsole = new VConsole()
        } else {
          window.vConsole && window.vConsole.destroy()
        }
      }
    },
    mounted() {
      console.log('触发mounted')
      if (window.innerHeight > currentHeight) {
        currentHeight = window.innerHeight
        document.getElementById('app').style.height = window.innerHeight + 'px'
      }
      let _this = this
      this.loading = true
      setTimeout(function () {
        _this.loadLogs()
      }, 200)
    }
  })
</script>

</html>